﻿using System;
using Affirma.ThreeSharp;
using Affirma.ThreeSharp.Query;
using Affirma.ThreeSharp.Model;
using System.Xml;
using System.Collections.Generic;
using System.Xml.Serialization;
using System.IO;
using System.Net;
using TimHeuer.Expression.S3Publish.S3Model;

namespace TimHeuer.Expression.S3Publish
{
    /// <summary>
    /// Wraps basic functionality of the ThreeSharp Library into single-line commands
    /// </summary>
    public class ThreeSharpWrapper
    {
        private const string ACL_HEADER = "x-amz-acl";
        private const string ACL_PUBLIC_READ = "public-read";
        private const string ACL_PUBLIC_READ_WRITE = "public-read-write";

        private ThreeSharpConfig _config;
        private IThreeSharp _service;

        private static XmlSerializer _bucketListSerializer = new XmlSerializer(typeof(ListBucketResult));

        public ThreeSharpWrapper(String awsAccessKeyId, String awsSecretAccessKey)
        {
            _config = new ThreeSharpConfig();
            _config.AwsAccessKeyID = awsAccessKeyId;
            _config.AwsSecretAccessKey = awsSecretAccessKey;

            _service = new ThreeSharpQuery(_config);
        }

        /// <summary>
        /// Gets or sets whether to use a secure connection.
        /// </summary>
        public bool IsSecure
        {
            get { return _config.IsSecure; }
            set { _config.IsSecure = value; }
        }

        public bool UsePublicUri
        {
            get { return _config.UsePublicUri; }
            set { _config.UsePublicUri = value; }
        }

        public IThreeSharp Service
        {
            get { return _service; }
        }

        /// <summary>
        /// Adds a bucket to an S3 account
        /// </summary>
        public void AddBucket(String bucketName)
        {
            BucketAddRequest bucketAddRequest = new BucketAddRequest(bucketName);
            BucketAddResponse bucketAddResponse = _service.BucketAdd(bucketAddRequest);
            bucketAddResponse.DataStream.Close();
        }

        /// <summary>
        /// Returns an array of <see cref="S3Bucket"/>s representing all configured buckets.
        /// </summary>
        public IList<S3Bucket> ListAllBuckets()
        {
            List<S3Bucket> bucketList = new List<S3Bucket>();
            try
            {
                BucketListRequest bucketListRequest = new BucketListRequest(null);
                BucketListResponse bucketListResponse = _service.BucketList(bucketListRequest);
                XmlDocument xmlDoc = bucketListResponse.StreamResponseToXmlDocument();
                XmlNodeList buckets = xmlDoc.SelectNodes("//*[local-name()='Name']");
                foreach (XmlNode bucket in buckets)
                {
                    bucketList.Add(new S3Bucket(bucket.InnerText));
                }
            }
            catch (WebException webEx)
            {
                throw new S3Exception(webEx.Message, webEx);
            }
            return bucketList;
        }

        /// <summary>
        /// Returns an array of <see cref="S3Object"/>s describing the contents of the bucket.
        /// </summary>
        public IList<S3Object> ListBucket(string bucketName)
        {
            List<S3Object> objectList = new List<S3Object>();
            S3Bucket bucket = new S3Bucket(bucketName);
            BucketListRequest request = null;
            BucketListResponse response = null;

            // The first thing we need to do is check for the presence of a Temporary Redirect.  These occur for a few
            // minutes after an EU bucket is created, while S3 creates the DNS entries.  If we get one, we need to pull
            // the bucket listing from the redirect URL
            String redirectUrl = null;
            BucketListRequest testRequest = new BucketListRequest(bucketName);
            testRequest.Method = "HEAD";
            BucketListResponse testResponse = _service.BucketList(testRequest);
            testResponse.DataStream.Close();
            if (testResponse.StatusCode == System.Net.HttpStatusCode.TemporaryRedirect)
            {
                redirectUrl = testResponse.Headers["Location"].ToString();
            }

            bool isTruncated = true;
            string marker = String.Empty;

            // The while-loop is here because S3 will only return a maximum of 1000 items at a time, so if the list
            // was truncated, we need to make another call and get the rest
            while (isTruncated)
            {
                isTruncated = false; // Reset to prevent accidental infinite loops
                
                request = new BucketListRequest(bucketName);
                request.RedirectUrl = redirectUrl;
                if (!String.IsNullOrEmpty(marker))
                {
                    request.QueryList.Add("marker", marker);
                }
                response = _service.BucketList(request);

                string xml = response.StreamResponseToString();
                if (!String.IsNullOrEmpty(xml))
                {
                    StringReader stringReader = new StringReader(xml);
                    ListBucketResult listBucketResult = _bucketListSerializer.Deserialize(stringReader) as ListBucketResult;
                    if (listBucketResult != null)
                    {
                        isTruncated = Convert.ToBoolean(listBucketResult.IsTruncated);
                        foreach (ListBucketResultContents content in listBucketResult.Contents)
                        {
                            S3Object s3Obj = new S3Object(content);
                            s3Obj.Bucket = bucket;
                            objectList.Add(s3Obj);

                            marker = s3Obj.Key;
                        }
                    }
                }
            }

            return objectList;
        }

        private S3Object S3ObjectFromContent(ListBucketResultContents content)
        {
            if (content == null)
            {
                return null;
            }

            S3Object s3 = new S3Object();
            throw new NotImplementedException();
        }

        private S3Object S3ObjectFromXml(XmlNode node)
        {
            S3Object s3 = new S3Object();
            s3.Key = node["Key"].InnerText;
            s3.ParseAndSetLastModified(node["LastModified"].InnerText);
            s3.Size = node["Size"].InnerText;
            s3.Owner = node["Owner"]["DisplayName"].InnerText;
            s3.StorageClass = node["StorageClass"].InnerText;
            return s3;
        }

        /// <summary>
        /// Adds a string to a bucket, as an object
        /// </summary>
        public void AddStringObject(String bucketName, String keyName, String data)
        {
            ObjectAddRequest objectAddRequest = new ObjectAddRequest(bucketName, keyName);
            objectAddRequest.LoadStreamWithString(data);
            ObjectAddResponse objectAddResponse = _service.ObjectAdd(objectAddRequest);
            objectAddResponse.DataStream.Close();
        }

        /// <summary>
        /// Starts the process of streaming a file to a bucket as an object
        /// </summary>
        public ObjectAddRequest StartAddFileObject(String bucketName, String keyName, String localfile, String contentType)
        {
            ObjectAddRequest objectAddRequest = new ObjectAddRequest(bucketName, keyName);
            objectAddRequest.Headers.Add(ACL_HEADER, ACL_PUBLIC_READ);
            objectAddRequest.Headers.Add("Expires", DateTime.Now.AddMonths(1).ToUniversalTime().ToString("R"));
            if (!string.IsNullOrEmpty(contentType))
            {
                objectAddRequest.Headers.Add("Content-Type", contentType);
            }
            objectAddRequest.LoadStreamWithFile(localfile);
            return objectAddRequest;
        }

        /// <summary>
        /// Ends the process of streaming a file to a bucket as an object
        /// </summary>
        public void EndAddFileObject(ObjectAddRequest addRequest)
        {
            ObjectAddResponse objectAddResponse = _service.ObjectAdd(addRequest);
            objectAddResponse.DataStream.Close();
        }

        /// <summary>
        /// Streams a file to a bucket as an object
        /// </summary>
        public void AddFileObject(String bucketName, String keyName, String localfile, String contentType)
        {
            EndAddFileObject(StartAddFileObject(bucketName, keyName, localfile, contentType));
        }

        /// <summary>
        /// Streams a file to a bucket as an object, with encryption
        /// </summary>
        public void AddEncryptFileObject(String bucketName, String keyName, String localfile, String encryptionKey, String encryptionIV)
        {
            ObjectAddRequest objectAddRequest = new ObjectAddRequest(bucketName, keyName);
            objectAddRequest.LoadStreamWithFile(localfile);
            objectAddRequest.EncryptStream(encryptionKey, encryptionIV);
            ObjectAddResponse objectAddResponse = _service.ObjectAdd(objectAddRequest);
            objectAddResponse.DataStream.Close();
        }

        /// <summary>
        /// Gets a file object from a bucket, and streams it to disk
        /// </summary>
        public void GetFileObject(String bucketName, String keyName, String localfile)
        {
            ObjectGetRequest objectGetRequest = new ObjectGetRequest(bucketName, keyName);
            ObjectGetResponse objectGetResponse = _service.ObjectGet(objectGetRequest);
            objectGetResponse.StreamResponseToFile(localfile);
        }

        /// <summary>
        /// Gets a file object from a bucket, streaming it to disk, with decryption
        /// </summary>
        public void GetDecryptFileObject(String bucketName, String keyName, String localfile, String encryptionKey, String encryptionIV)
        {
            ObjectGetRequest objectGetRequest = new ObjectGetRequest(bucketName, keyName);
            ObjectGetResponse objectGetResponse = _service.ObjectGet(objectGetRequest);
            objectGetResponse.DecryptStream(encryptionKey, encryptionIV);
            objectGetResponse.StreamResponseToFile(localfile);
        }

        /// <summary>
        /// Generates a URL to access an S3 object in a bucket
        /// </summary>
        public String GetUrl(String bucketName, String keyName)
        {
            UrlGetRequest urlGetRequest = new UrlGetRequest(bucketName, keyName);
            urlGetRequest.ExpiresIn = 60 * 1000;
            UrlGetResponse urlGetResponse = _service.UrlGet(urlGetRequest);
            return urlGetResponse.StreamResponseToString();
        }

        /// <summary>
        /// Deletes an object from a bucket
        /// </summary>
        public void DeleteObject(String bucketName, String keyName)
        {
            ObjectDeleteRequest objectDeleteRequest = new ObjectDeleteRequest(bucketName, keyName);
            ObjectDeleteResponse objectDeleteResponse = _service.ObjectDelete(objectDeleteRequest);
            objectDeleteResponse.DataStream.Close();
        }

        /// <summary>
        /// Deletes a bucket from an S3 account
        /// </summary>
        public void DeleteBucket(String bucketName)
        {
            BucketDeleteRequest bucketDeleteRequest = new BucketDeleteRequest(bucketName);
            BucketDeleteResponse bucketDeleteResponse = _service.BucketDelete(bucketDeleteRequest);
            bucketDeleteResponse.DataStream.Close();
        }
    }
}
